package com.googlecode.jmicroerp.repository;

import com.googlecode.jmicroerp.model.Entitate;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

/**
 *
 * @author alex
 */
@Repository(value = "genericRepository")
public class GenericRepository<T extends Entitate> implements Serializable {
private static final long serialVersionUID = 1L;
    @PersistenceContext
    private EntityManager entityManager;

    private Predicate addWhereRestriction(CriteriaBuilder criteriaBuilder, Root<T> root, Predicate wherePredicate, GenericRestriction restriction) {
        GenericProperty property = restriction.getProperty();
        Path path = root;
        Join join = null;
        while (property.getReference() != null) {
            if (join == null) {
                join = root.join(property.getPropertyName(), JoinType.LEFT);
            } else {
                join = join.join(property.getPropertyName(), JoinType.LEFT);
            }
            path = path.get(property.getPropertyName());
            property = property.getReference();
        }
        Predicate predicate;
        if (join == null) {
            switch (restriction.getOp()) {                
                case LT:
                    predicate = criteriaBuilder.lessThan(root.<Comparable>get(property.getPropertyName()), restriction.getValue());
                    break;
                case LE:
                    predicate = criteriaBuilder.lessThanOrEqualTo(root.<Comparable>get(property.getPropertyName()), restriction.getValue());
                    break;
                case GT:
                    predicate = criteriaBuilder.greaterThan(root.<Comparable>get(property.getPropertyName()), restriction.getValue());
                    break;
                case GE:
                    predicate = criteriaBuilder.greaterThanOrEqualTo(root.<Comparable>get(property.getPropertyName()), restriction.getValue());
                    break;
                case LK:
                    String expression = "";
                    if (restriction.getValue() != null) {
                        expression = restriction.getValue().toString();
                    }
                    predicate = criteriaBuilder.like(criteriaBuilder.upper(root.<String>get(property.getPropertyName())), "%" + expression.toUpperCase() + "%");
                    break;
                case IN:
                    Expression expIn = root.<String>get(property.getPropertyName());
                    predicate = expIn.in(restriction.getListValue());
                    break;
                case NOT_NULL:
                    predicate = criteriaBuilder.isNotNull(root.<String>get(property.getPropertyName()));
                    break;
                case IS_NULL:
                    predicate = criteriaBuilder.isNull(root.<String>get(property.getPropertyName()));
                    break;
                case EQ:
                default:
                    predicate = criteriaBuilder.equal(root.<String>get(property.getPropertyName()), restriction.getValue());
                    break;
            }
        } else {
            switch (restriction.getOp()) {
                case LT:
                    predicate = criteriaBuilder.lessThan(join.get(property.getPropertyName()), restriction.getValue());
                    break;
                case LE:
                    predicate = criteriaBuilder.lessThanOrEqualTo(join.get(property.getPropertyName()), restriction.getValue());
                    break;
                case GT:
                    predicate = criteriaBuilder.greaterThan(join.get(property.getPropertyName()), restriction.getValue());
                    break;
                case GE:
                    predicate = criteriaBuilder.greaterThanOrEqualTo(join.get(property.getPropertyName()), restriction.getValue());
                    break;
                case LK:
                    predicate = criteriaBuilder.like(criteriaBuilder.upper(join.get(property.getPropertyName())), "%" + restriction.getValue().toString().toUpperCase() + "%");
                    break;
                case IN:
                    Expression expIn = join.get(property.getPropertyName());
                    predicate = expIn.in(restriction.getListValue());
                    break;
                case NOT_NULL:
                    predicate = criteriaBuilder.isNotNull(join.get(property.getPropertyName()));
                    break;
                case IS_NULL:
                    predicate = criteriaBuilder.isNull(join.get(property.getPropertyName()));
                    break;
                case EQ:
                default:
                    predicate = criteriaBuilder.equal(join.get(property.getPropertyName()), restriction.getValue());
                    break;
            }
        }
        if (wherePredicate == null) {
            wherePredicate = predicate;
        } else {
            wherePredicate = criteriaBuilder.and(wherePredicate, predicate);
        }
        
        return wherePredicate;
    }

    public List<T> getList(Class<T> persistentClass, long firstResult, long maxResults, List<GenericRestriction> restrictions, List<GenericOrder> orders) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(persistentClass);
        Root<T> root = criteriaQuery.from(persistentClass);
        Predicate wherePredicate = null;
        criteriaQuery.select(root);
        if (restrictions != null) {
            for (GenericRestriction restriction : restrictions) {
                wherePredicate = addWhereRestriction(criteriaBuilder, root, wherePredicate, restriction);
            }
            if (wherePredicate != null) {
                criteriaQuery.where(wherePredicate);
            }
        }
        if (orders != null) {
            List<Order> criteriaOrder = new ArrayList<Order>();
            for (GenericOrder order : orders) {
                GenericProperty property = order.getProperty();
                Path path = root;
                while (property != null) {
                    path = path.get(property.getPropertyName());
                    property = property.getReference();
                }
                if (order.isAsc()) {
                    criteriaOrder.add(criteriaBuilder.asc(path));
                } else {
                    criteriaOrder.add(criteriaBuilder.desc(path));
                }
            }
            criteriaQuery.orderBy(criteriaOrder);
        }

        return entityManager.createQuery(criteriaQuery)
                .setFirstResult((int) firstResult)
                .setMaxResults((int) maxResults)
                .getResultList();
    }

    public T getSingleResult(Class<T> persistentClass, GenericRestriction... restrictions) {
        List<T> list = getList(persistentClass, 0, Integer.MAX_VALUE, Arrays.asList(restrictions), new ArrayList<GenericOrder>());

        if (list.size() == 1) {
            return list.get(0);
        } else if (list.isEmpty()) {
            return null;
        } else {
            throw new NonUniqueResultException("Non unique result for " + persistentClass.getCanonicalName());
        }
    }

    public Long getCount(Class<T> persistentClass, GenericRestriction... restrictions) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
        Root root = criteriaQuery.from(persistentClass);
        Predicate wherePredicate = null;
        criteriaQuery.select(root);
        if (restrictions != null) {
            for (GenericRestriction restriction : restrictions) {
                wherePredicate = addWhereRestriction(criteriaBuilder, root, wherePredicate, restriction);
            }
            if (wherePredicate != null) {
                criteriaQuery.where(wherePredicate);
            }
        }

        criteriaQuery.select(criteriaBuilder.count(root));

        return entityManager.createQuery(criteriaQuery).getSingleResult();
    }

    public T getById(Class<T> persistentClass, Long id) {
        return entityManager.getReference(persistentClass, id);
    }

    @Transactional
    public void update(T entitate) {
        entityManager.merge(entitate);
    }

    @Transactional
    public void insert(T entitate) {
        entityManager.persist(entitate);
    }

    @Transactional
    public void save(T entitate) {
        if (entitate.getId() == null) {
            insert(entitate);
        } else {
            update(entitate);
        }
    }
    
    @Transactional
    public void delete(T entitate) {
        entityManager.remove(entitate);
    }
}
